/*
 *  tracker/PianoControl.cpp
 *
 *  Copyright 2009 Peter Barth
 *
 *  This file is part of Milkytracker.
 *
 *  Milkytracker is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Milkytracker is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Milkytracker.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "PianoControl.h"
#include "Screen.h"
#include "GraphicsAbstract.h"
#include "Font.h"
#include "ScrollBar.h"
#include "Piano.h"
#include "Tools.h"
#include "PatternTools.h"
#include "TrackerConfig.h"

#define SCROLLBARWIDTH SCROLLBUTTONSIZE

static const bool blackKeys[] =
{
    false,
    true,
    false,
    true,
    false,
    false,
    true,
    false,
    true,
    false,
    true,
    false
};

static const PPPoint positions[] =
{
    PPPoint(1, 17),
    PPPoint(5, 6),
    PPPoint(9, 17),
    PPPoint(13, 6),
    PPPoint(17, 17),
    PPPoint(25, 17),
    PPPoint(29, 6),
    PPPoint(33, 17),
    PPPoint(37, 6),
    PPPoint(41, 17),
    PPPoint(45, 6),
    PPPoint(49, 17)
};

static const PPColor colors[] =
{
    PPColor(0, 0, 0),
    PPColor(255, 255, 255),
    PPColor(0, 0, 0),
    PPColor(255, 255, 255),
    PPColor(0, 0, 0),
    PPColor(0, 0, 0),
    PPColor(255, 255, 255),
    PPColor(0, 0, 0),
    PPColor(255, 255, 255),
    PPColor(0, 0, 0),
    PPColor(255, 255, 255),
    PPColor(0, 0, 0)
};

pp_int32 PianoControl::XMAX()
{
    return pianoBitmap->getBitmapWidth();
}

pp_int32 PianoControl::YMAX()
{
    return pianoBitmap->getBitmapHeight();
}

pp_int32 PianoControl::KEYWIDTH()
{
    return XMAX() / pianoBitmap->getBitmapLUTWidth();
}

PianoControl::PianoControl (pp_int32 id,
                            PPScreen* parentScreen,
                            EventListenerInterface* eventListener,
                            const PPPoint& location,
                            const PPSize& size,
                            pp_uint8 numNotes,
                            bool border /*= true*/) :
    PPControl(id, parentScreen, eventListener, location, size),
    border(border),
    NUMNOTES(numNotes),
    borderColor(&ourOwnBorderColor),
    mode(ModeEdit)
{
    // default color
    ourOwnBorderColor.set(192, 192, 192);

    hScrollbar = new PPScrollbar(0, parentScreen, this, PPPoint(location.x, location.y + size.height - SCROLLBARWIDTH - 1), size.width - 1, true);

    #ifndef __LOWRES__
    xscale = 1;
    yscale = 1;
    pianoBitmap = PianoBitmapLarge::getInstance();
    #else
    xscale = 2;
    yscale = 1;
    pianoBitmap = PianoBitmapSmall::getInstance();
    #endif

    xMax = XMAX() * xscale;
    yMax = YMAX() * yscale;

    visibleWidth = size.width - 2;
    visibleHeight = size.height - SCROLLBARWIDTH - 2;

    adjustScrollbars();

    startPos = 0;

    caughtControl = NULL;
    controlCaughtByLMouseButton = controlCaughtByRMouseButton = false;

    nbu = new pp_uint8[NUMNOTES];
    memset(nbu, 0, NUMNOTES);

    keyState = new KeyState[NUMNOTES];

    sampleIndex = 0;
}

PianoControl::~PianoControl ()
{
    delete hScrollbar;

    delete[] nbu;

    delete[] keyState;
}

void PianoControl::paint(PPGraphicsAbstract* g)
{
    if (!isVisible())
        return;

    pp_int32 xOffset = 2;

    pp_int32 yOffset = 2;

    g->setRect(location.x, location.y, location.x + size.width, location.y + size.height);

    if (border)
    {
        drawBorder(g, *borderColor);
    }

    g->setRect(location.x + 1, location.y + 1, location.x + size.width - 2, location.y + size.height - 2);

    pp_int32 width = visibleWidth;

    pp_int32 adder = 65536 / xscale;

    pp_int32 oy = location.y + 1;

    // for black piano keys
    PPColor colCorrect(TrackerConfig::colorThemeMain);
    colCorrect.r <<= 1; colCorrect.g <<= 1; colCorrect.b <<= 1;
    // for white piano keys
    PPColor colCorrect2(TrackerConfig::colorThemeMain);
    colCorrect2.scale(1.5f, 1.5f, 1.6f);
    pp_int32 avg = (colCorrect2.b + colCorrect2.g + colCorrect2.r) / 3;
    PPColor colCorrect4(avg, avg, avg);

    pp_int32 PIANO_LUT_WIDTH = pianoBitmap->getBitmapLUTWidth();
    const pp_uint8* PIANO_LUT = pianoBitmap->getBitmapLUT();
    const pp_uint8* PIANO = pianoBitmap->getBitmap();

    const pp_int32* DIVLUT = pianoBitmap->getDIVLUT();
    const pp_int32* MODLUT = pianoBitmap->getMODLUT();

    const pp_int32 XMAX = this->XMAX();

    for (pp_int32 y = 0; y < visibleHeight; y++)
    {
        pp_int32 ry = y / yscale;

        pp_int32 offset = (ry * XMAX + (startPos / xscale));
        pp_int32 ofs = 0;

        const pp_uint8* src = PIANO_LUT + (ry * PIANO_LUT_WIDTH * 3);

        const pp_int32* divLutPtr = DIVLUT - ry * XMAX;
        const pp_int32* modLutPtr = MODLUT - ry * XMAX;

        pp_int32 ox = location.x + 1;
        for (pp_int32 x = 0; x < width; x++)
        {
            pp_int32 sx = offset + (ofs >> 16);
            pp_int32 px = modLutPtr[sx];

            pp_int32 c = src[px];
            pp_int32 gr = src[px + 1];
            pp_int32 b = src[px + 2];

            if (c == 255 && gr == 0 && b == 0)
            {
                g->setColor(PIANO[sx], PIANO[sx], PIANO[sx]);
            }
            else
            {
                // color values equal/above 240
                if (c >= 240) c -= 240;

                pp_int32 note = divLutPtr[sx] + c;

                if (keyState[note].pressed)
                {
                    if (keyState[note].muted)
                    {
                        if (colors[c].r)
                            g->setSafeColor((PIANO[sx] + (colCorrect4.r >> 2)), (PIANO[sx] + (colCorrect4.g >> 2)), (PIANO[sx] + (colCorrect4.b >> 2)));
                        else
                            g->setSafeColor((PIANO[sx] * colCorrect4.r) >> 8, (PIANO[sx] * colCorrect4.g) >> 8, (PIANO[sx] * colCorrect4.b) >> 8);
                    }
                    else
                    {
                        if (colors[c].r)
                            g->setSafeColor((PIANO[sx] + (colCorrect.r >> 1)), (PIANO[sx] + (colCorrect.g >> 1)), (PIANO[sx] + (colCorrect.b >> 1)));
                        else
                            g->setSafeColor((PIANO[sx] * colCorrect2.r) >> 8, (PIANO[sx] * colCorrect2.g) >> 8, (PIANO[sx] * colCorrect2.b) >> 8);
                    }
                }
                else
                    g->setColor(PIANO[sx], PIANO[sx], PIANO[sx]);
            }
            g->setPixel(ox, oy);
            ofs += adder;
            ox++;
        }
        oy++;
    }

    float newXScale = pianoBitmap->getBitmapWidth() / PianoBitmapSmall::getInstance()->getBitmapWidth();
    float newYScale = pianoBitmap->getBitmapHeight() / PianoBitmapSmall::getInstance()->getBitmapHeight();

    if (mode == ModeEdit)
    {
        PPFont* font;

        font = (xscale == 1 && pianoBitmap == PianoBitmapSmall::getInstance()) ?
               PPFont::getFont(PPFont::FONT_TINY) : PPFont::getFont(PPFont::FONT_SYSTEM);

        g->setFont(font);

        xOffset = 0;
        yOffset = -1;
        if (xscale > 1)
        {
            xOffset = 2;
        }
        else if (pianoBitmap == PianoBitmapLarge::getInstance())
        {
            xOffset = 1;
        }

        if (yscale > 1)
        {
            yOffset = -2;
        }
        else if (pianoBitmap == PianoBitmapLarge::getInstance())
        {
            yOffset = 0;
        }

        for (pp_int32 i = 0; i < NUMNOTES; i++)
        {
            pp_int32 posx = location.x + 1 + (i / 12) * (PIANO_LUT_WIDTH * xscale) - startPos;

            //if (blackKeys[i%12] && pianoBitmap == PianoBitmapLarge::getInstance())
            //	posx;

            g->setColor(colors[i % 12]);

            char str[3] = "-";

            if (nbu[i] != 255)
                PPTools::convertToHex(str, nbu[i], 1);

            g->drawChar(str[0],
                        (pp_int32)(posx + (positions[i % 12].x * xscale + xOffset) * newXScale),
                        (pp_int32)(location.y + 1 + (positions[i % 12].y * yscale + yOffset) * newYScale));
        }

    }
    else if (mode == ModePlay && (xscale >= 2 || pianoBitmap == PianoBitmapLarge::getInstance()))
    {
        PPFont* font = PPFont::getFont(PPFont::FONT_TINY);

        g->setFont(font);

        if (pianoBitmap == PianoBitmapLarge::getInstance())
        {
            xOffset = 1;
            yOffset = 0;
        }
        else
        {
            xOffset = 1;
            yOffset = -1;
        }

        for (pp_int32 i = 0; i < NUMNOTES; i++)
        {
            pp_int32 posx = location.x + 1 + (i / 12) * (PIANO_LUT_WIDTH * xscale) - startPos;

            if (blackKeys[i % 12] && pianoBitmap == PianoBitmapLarge::getInstance())
                posx--;

            g->setColor(colors[i % 12]);

            char str[4] /* = "C#"*/;
            PatternTools::getNoteName(str, i + 1);
            if (str[1] == '-')
            {
                str[1] = str[2];
                str[2] = '\0';
                xOffset = 0;
            }
            else
            {
                str[2] = '\0';
                xOffset = 1;
            }

            pp_int32 correctx = (KEYWIDTH() * xscale) / 2 - font->getStrWidth(str) / 2 - 3;
            if (correctx < 0)
                correctx = 0;

            xOffset += correctx;

            g->drawString(str,
                          (pp_int32)(posx + (positions[i % 12].x * xscale + xOffset) * newXScale),
                          (pp_int32)(location.y + 1 + (positions[i % 12].y * yscale + yOffset) * newYScale));
        }
    }

    hScrollbar->paint(g);

}

pp_int32 PianoControl::dispatchEvent(PPEvent* event)
{
    if (eventListener == NULL)
        return -1;

    //if (!visible)
    //	return 0;

    switch (event->getID())
    {
        case eRMouseDown:
        {
            PPPoint* p = (PPPoint*)event->getDataPtr();

            if (hScrollbar->hit(*p))
            {
                if (controlCaughtByLMouseButton)
                    break;
                controlCaughtByRMouseButton = true;
                caughtControl = hScrollbar;
                caughtControl->dispatchEvent(event);
            }
            break;
        }

        case eLMouseDown:
        {
            PPPoint* p = (PPPoint*)event->getDataPtr();

            // Clicked on horizontal scrollbar -> route event to scrollbar and catch scrollbar control
            if (hScrollbar->hit(*p))
            {
                if (controlCaughtByRMouseButton)
                    break;
                controlCaughtByLMouseButton = true;
                caughtControl = hScrollbar;
                caughtControl->dispatchEvent(event);
            }
            // Clicked in client area
            else
            {

                pp_int32 note = positionToNote(*p);

                if (note != -1)
                {

                    switch (mode)
                    {
                        case ModeEdit:
                        {
                            nbu[note] = (pp_uint8)sampleIndex & 0xf;
                            PPEvent e(eValueChanged, &nbu, sizeof(pp_uint8*));
                            eventListener->handleEvent(reinterpret_cast<PPObject*>(this), &e);
                            break;
                        }

                        case ModePlay:
                        {
                            currentSelectedNote = note;
                            PPEvent e(eSelection, &note, sizeof(note));
                            eventListener->handleEvent(reinterpret_cast<PPObject*>(this), &e);
                            break;
                        }
                    }

                    parentScreen->paintControl(this);
                }
            }

            break;
        }

        case eRMouseUp:
        {
            controlCaughtByRMouseButton = false;
            if (caughtControl && !controlCaughtByLMouseButton && !controlCaughtByRMouseButton)
            {
                caughtControl->dispatchEvent(event);
                caughtControl = NULL;
                break;
            }
            break;
        }

        case eLMouseUp:
            if (caughtControl == NULL)
            {
                pp_int32 note = currentSelectedNote;

                if (note != -1)
                {

                    switch (mode)
                    {
                        case ModePlay:
                        {
                            pp_int32 v = (1 << 16) + currentSelectedNote;
                            PPEvent e(eSelection, &v, sizeof(v));
                            eventListener->handleEvent(reinterpret_cast<PPObject*>(this), &e);
                            break;
                        }
                    }

                    parentScreen->paintControl(this);
                }
                break;
            }

            controlCaughtByLMouseButton = false;
            if (!controlCaughtByLMouseButton && !controlCaughtByRMouseButton)
            {
                caughtControl->dispatchEvent(event);
                caughtControl = NULL;
            }
            break;

        case eLMouseDrag:
        {
            if (caughtControl && controlCaughtByLMouseButton)
            {
                caughtControl->dispatchEvent(event);
                break;
            }

            //positionToNote(*(PPPoint*)event->getDataPtr());

            //parentScreen->paintControl(this);

            pp_int32 note = positionToNote(*(PPPoint*)event->getDataPtr());

            if (note != -1)
            {

                switch (mode)
                {
                    case ModeEdit:
                    {
                        nbu[note] = (pp_uint8)sampleIndex & 0xf;

                        PPEvent e(eValueChanged, &nbu, sizeof(pp_uint8*));

                        eventListener->handleEvent(reinterpret_cast<PPObject*>(this), &e);
                        break;
                    }

                    case ModePlay:
                    {
                        if (note == currentSelectedNote)
                            break;

                        pp_int32 v = (1 << 16) + currentSelectedNote;
                        PPEvent e(eSelection, &v, sizeof(v));
                        eventListener->handleEvent(reinterpret_cast<PPObject*>(this), &e);

                        currentSelectedNote = note;
                        PPEvent e2(eSelection, &note, sizeof(note));
                        eventListener->handleEvent(reinterpret_cast<PPObject*>(this), &e2);
                        break;
                    }
                }

                parentScreen->paintControl(this);
            }

            break;
        }

        case eRMouseDrag:
        {
            if (caughtControl && controlCaughtByRMouseButton)
                caughtControl->dispatchEvent(event);
            break;
        }

        case eRMouseRepeat:
        {
            if (caughtControl && controlCaughtByRMouseButton)
                caughtControl->dispatchEvent(event);
            break;
        }

        case eMouseWheelMoved:
        {
            TMouseWheelEventParams* params = (TMouseWheelEventParams*)event->getDataPtr();

            if (params->delta > 0 && hScrollbar)
            {
                PPEvent e(eBarScrollUp);
                handleEvent(reinterpret_cast<PPObject*>(hScrollbar), &e);
            }
            else if (params->delta < 0 && hScrollbar)
            {
                PPEvent e(eBarScrollDown);
                handleEvent(reinterpret_cast<PPObject*>(hScrollbar), &e);
            }

            event->cancel();

            break;
        }

        default:
            if (caughtControl == NULL)
                break;

            caughtControl->dispatchEvent(event);
            break;

    }

    return 0;
}

pp_int32 PianoControl::handleEvent(PPObject* sender, PPEvent* event)
{
    // Horizontal scrollbar, scroll up (=left)
    if (sender == reinterpret_cast<PPObject*>(hScrollbar) &&
        event->getID() == eBarScrollUp)
    {
        startPos -= KEYWIDTH() * xscale;
        if (startPos < 0)
            startPos = 0;

        pp_int32 visibleItems = visibleWidth;

        float v = (float)(getMaxWidth() - visibleItems);

        hScrollbar->setBarPosition((pp_int32)(startPos * (65536.0f / v)));
    }
    // Horizontal scrollbar, scroll down (=right)
    else if (sender == reinterpret_cast<PPObject*>(hScrollbar) &&
             event->getID() == eBarScrollDown)
    {
        pp_int32 visibleItems = visibleWidth;

        startPos += KEYWIDTH() * xscale;
        if (startPos + visibleItems >= (signed)getMaxWidth())
            startPos = getMaxWidth() - visibleItems;

        float v = (float)(getMaxWidth() - visibleItems);

        hScrollbar->setBarPosition((pp_int32)(startPos * (65536.0f / v)));
    }
    // Horizontal scrollbar, position changed
    else if (sender == reinterpret_cast<PPObject*>(hScrollbar) &&
             event->getID() == eBarPosChanged)
    {

        float pos = hScrollbar->getBarPosition() / 65536.0f;

        pp_int32 visibleItems = visibleWidth;

        float v = (float)(getMaxWidth() - visibleItems);

        startPos = (pp_uint32)(v * pos);
    }

    parentScreen->paintControl(this);

    return 0;
}

pp_int32 PianoControl::getMaxWidth()
{
    return XMAX() * xscale;
}

void PianoControl::adjustScrollbars()
{
    float s = (float)visibleWidth / (float)getMaxWidth();

    float olds = hScrollbar->getBarSize() / 65536.0f;

    hScrollbar->setBarSize((pp_int32)(s * 65536.0f), false);

    s = hScrollbar->getBarSize() / 65536.0f;

    float scale = s / olds;

    float pos = hScrollbar->getBarPosition() / 65536.0f;
    hScrollbar->setBarPosition((pp_int32)(pos * scale * 65536.0f));

    pos = hScrollbar->getBarPosition() / 65536.0f;

    pp_int32 visibleItems = visibleWidth;

    float v = (float)(getMaxWidth() - visibleItems);

    startPos = (pp_uint32)(v * pos);

    if (startPos < 0)
    {
        startPos = 0;
    }

    //pp_int32 entireSize = (horizontal?this->size.width:this->size.height) - SCROLLBUTTONSIZE*2;
}

void PianoControl::setxScale(pp_int32 scale)
{
    xscale = scale;
    adjustScrollbars();
    assureNoteVisible(4 * 12);
}

void PianoControl::setyScale(pp_int32 scale)
{
    yscale = scale;
}

void PianoControl::setLocation(const PPPoint& location)
{
    PPControl::setLocation(location);

    pp_int32 p = hScrollbar->getBarPosition();
    pp_int32 s = hScrollbar->getBarSize();
    delete hScrollbar;
    hScrollbar = new PPScrollbar(0, parentScreen, this, PPPoint(location.x, location.y + size.height - SCROLLBARWIDTH - 1), size.width - 1, true);
    hScrollbar->setBarSize(s);
    hScrollbar->setBarPosition(p);
}

void PianoControl::setSize(const PPSize& size)
{
    PPControl::setSize(size);
    delete hScrollbar;

    hScrollbar = new PPScrollbar(0, parentScreen, this, PPPoint(location.x, location.y + size.height - SCROLLBARWIDTH - 1), size.width - 1, true);

    visibleWidth = size.width - 2;
    visibleHeight = size.height - SCROLLBARWIDTH - 2;

    adjustScrollbars();
    assureNoteVisible(4 * 12);
}

void PianoControl::setSampleTable(const pp_uint8* nbu)
{
    if (nbu == NULL)
    {
        memset(this->nbu, 0, NUMNOTES);
        return;
    }

    memcpy(this->nbu, nbu, NUMNOTES);
}

void PianoControl::pressNote(pp_int32 note, bool pressed, bool muted /* = false*/)
{
    if (note >= 0 && note < NUMNOTES)
    {
        keyState[note].pressed = pressed;
        keyState[note].muted = muted;
    }
}

bool PianoControl::getNoteState(pp_int32 note) const
{
    if (note >= 0 && note < NUMNOTES)
        return keyState[note].pressed;
    return false;
}

void PianoControl::assureNoteVisible(pp_int32 note)
{
    pp_int32 PIANO_LUT_WIDTH = pianoBitmap->getBitmapLUTWidth();

    pp_int32 startPos = (PIANO_LUT_WIDTH * (note / 12) + positions[note % 12].x) * xscale;
    float v = (float)(getMaxWidth() - visibleWidth);
    hScrollbar->setBarPosition((pp_int32)(startPos * (65536.0f / v)));
    float pos = hScrollbar->getBarPosition() / 65536.0f;
    startPos = (pp_uint32)(v * pos);
    this->startPos = startPos;
}

pp_int32 PianoControl::positionToNote(PPPoint cp)
{

    if (sampleIndex < 0)
        return -1;

    cp.x -= location.x + 1;
    cp.y -= location.y + 1;

    if (cp.x < 0 || cp.x >= visibleWidth || cp.y < 0 || cp.y >= visibleHeight)
        return -1;

    pp_int32 PIANO_LUT_WIDTH = pianoBitmap->getBitmapLUTWidth();
    const pp_uint8* PIANO_LUT = pianoBitmap->getBitmapLUT();

    cp.x /= xscale;
    cp.y /= yscale;

    cp.x += startPos / xscale;

    pp_int32 octave = cp.x / PIANO_LUT_WIDTH;
    pp_int32 ox = cp.x % PIANO_LUT_WIDTH;

    pp_int32 c = PIANO_LUT[(cp.y * PIANO_LUT_WIDTH + ox) * 3];
    pp_int32 g = PIANO_LUT[(cp.y * PIANO_LUT_WIDTH + ox) * 3 + 1];
    pp_int32 b = PIANO_LUT[(cp.y * PIANO_LUT_WIDTH + ox) * 3 + 2];

    if (c == 255 && g == 0 && b == 0)
        return -1;

    // color values equal/above 240
    if (c >= 240) c -= 240;

    pp_int32 note = octave * 12 + c;

    if (note < 0 || note >= NUMNOTES)
        return -1;

    return note;
}